# This file is part of Gehyra.
#
# Gehyra is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Gehyra is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gehyra.  If not, see <http://www.gnu.org/licenses/>.

"""
@package gehyra.common.ipv4
Defines utility classes for dealing with IPv4 address and transforming between
various representations.
$Id: ipv4.py 460 2011-02-11 19:33:54Z andyhhp@gmail.com $
"""

"""
@file gehyra/common/ipv4.py
Defines utility classes for dealing with IPv4 address and transforming between
various representations.
"""

import re, struct


class IPv4Address(object):
    """Class for manipulating IPv4 Addresses.
    Can deal with dotted decimal representation, packed structures
    and integers in string or int form.
    """

    ## regex for finding hexidecimal string representations
    hex_regex = re.compile(r"^(?:0x)?(?P<addr>[0-9a-fA-F]{8})$")

    ## regex for finding dotted decimal string representations 
    dotdec_regex = re.compile(r"^([0-9]{1,3})\.([0-9]{1,3})\."
                              "([0-9]{1,3})\.([0-9]{1,3})$")

    def __init__(self, addr = None):
        """Constructor.
        @param addr Can be various representations of an IPv4 address:
         - Integer number
         - String of a hexidecial number
         - String of a dotted decimal number
         - String packed structure
        @exception TypeError if addr can not be converted to an IPv4 Address
        @exception ValueError if addr is the correct type but is out of range
        """

        ## integer value of the IPv4 address.
        #  we store a long because 2**32 is a long on a 32bit machine
        self.ip = None

        if isinstance(addr, (int, long)):
            self.set_integer(addr)

        elif isinstance(addr, str):

            if IPv4Address.hex_regex.match(addr) is not None:
                self.set_hex_string(addr)
                return

            if IPv4Address.dotdec_regex.match(addr) is not None:
                self.set_common_string(addr)
                return

            try:
                self.set_packed_string(addr)
            except ValueError:
                pass
            else:
                return
            
            raise ValueError("Unrecognised IPv4 Address '%s'" % (addr,))

        else:
            raise TypeError("String or Integer expected.  Got %s" % type(addr))
        
    def __eq__(self, other):
        """overload the == operator"""

        if not isinstance(other, IPv4Address):
            try:
                other = IPv4Address(other)
            except Exception:
                return NotImplemented

        return self.ip == other.ip

    def __ne__(self, other):
        """overload the != operator"""

        if not isinstance(other, IPv4Address):
            try:
                other = IPv4Address(other)
            except Exception:
                return NotImplemented

        return self.ip != other.ip

    def set_integer(self, addr):
        """Set the IPv4 address using an integer value.
        @param addr integer IPv4 address
        @exception TypeError if addr is not an integer
        @exception ValueError if addr is outside the range 0 <= val < 2^32
        """

        if not isinstance(addr, (int, long)):
            raise TypeError("Expected Integer.  Got %s" % type(addr))

        if 0 <= addr < 2**32:
            self.ip = long(addr)
        else:
            raise ValueError("IPv4 address must be in range 0 <= val < 2^32")

    def get_integer(self):
        """Get the IPv4 address as an integer.
        @exception ValueError if self.ip has not been set
        @returns integer IPv4 address
        """

        if self.ip is None:
            raise ValueError("Address has not been set to any value")
        return self.ip

    def set_hex_string(self, addr):
        """Set the IPv4 address using a hexidecimal string.
        @param addr Hexidecimal string with an optional 0x prefix
        @exception TypeError if addr is not a string
        @exception ValueError if addr is not a valid hexidecimal representation
        or if the hexidecimal number is out outside the range 0 <= val < 2^32
        """

        if not isinstance(addr, str):
            raise TypeError("Expected String.  Got %s" % type(addr))

        res = IPv4Address.hex_regex.match(addr)
        if res is None:
            raise ValueError("Expected Hex value in string.  Got %s" % addr)

        self.set_integer(long(res.group('addr'), 16))

    def set_common_string(self, addr):
        """Set the IPv4 address using a dotted decimal string.
        @param addr Dotted decimal string
        @exception TypeError if addr is not a string
        @exception ValueError if addr is not a valid dotted decimal
        representation or if any of the octets are outside the range
        0 <= val <= 255
        """

        if not isinstance(addr, str):
            raise TypeError("Expected String.  Got %s" % type(addr))

        res = IPv4Address.dotdec_regex.match(addr)
        if res is None:
            raise ValueError("Expected dotted decimal string.  Got %s" % addr)
        
        octets = map(int, res.groups())

        for o in octets:
            if not (0 <= o <= 255):
                raise ValueError("Octet must be in range 0 <= val "
                                 "<= 255.  Got %s" % o)

        self._set_from_octets(octets)

    def set_packed_string(self, addr):
        """Set the IPv4 address using a packed structure.
        @param addr packed structure string
        @exception TypeError if addr is not a string
        @exception ValueError if addr is out of the range 0 <= val < 2^32
        """

        if not isinstance(addr, str):
            raise TypeError("Expected String.  Got %s" % type(addr))

        try:
            octets = struct.unpack("!4B", addr)
        except struct.error:
            raise ValueError("Packed value out of range")
        
        self._set_from_octets(octets)

    def get_hex_string(self, prefix=False, upper_case=False):
        """Get the IPv4 address as an hexidecmal string.
        @param prefix boolean value indicating whether a 0x prefix is wanted on
        the returned string
        @param upper_case boolean value indicating whether the a-f or A-F should
        be used in the returned string
        @exception ValueError if self.ip has not been set
        @returns hexidecimal string
        """
        
        if self.ip is None:
            raise ValueError("Address has not been set to any value")

        if upper_case:
            fmt = "%08X"
        else:
            fmt = "%08x"

        res = fmt % self.ip
        
        if prefix:
            return "0x" + res
        else:
            return res

    def get_common_string(self, expanded=False):
        """Get the IPv4 address as a dotted decimal string.
        @param expanded boolean value indicating whether to expand each octet
        string to be 3 characters with leading 0's where nessesary.
        @exception ValueError if self.ip has not been set
        @returns dotted decimal string
        """
        
        if self.ip is None:
            raise ValueError("Address has not been set to any value")
        
        octets = self._get_as_octets()
        
        if expanded:
            return '.'.join( "%03d" % o for o in octets )
        else:
            return '.'.join( "%d" % o for o in octets )

    def get_packed_string(self):
        """Get the IPv4 address as a packed string in network order.
        @exception ValueError if self.ip has not been set
        @returns packed string
        """

        if self.ip is None:
            raise ValueError("Address has not been set to any value")

        return struct.pack("!4B", *self._get_as_octets())

    def _set_from_octets(self, octets):
        """Private helper to set self.ip from a list of octets"""
        self.ip = long( (octets[0] << 24) |
                        (octets[1] << 16) |
                        (octets[2] << 8)  |
                         octets[3] )

    def _get_as_octets(self):
        """Private helper to get a list of octets from self.ip"""
        return [ (self.ip >> 24) & 0xff,
                 (self.ip >> 16) & 0xff,
                 (self.ip >> 8)  & 0xff,
                 (self.ip)       & 0xff ]

    # def __repr__(self):
    #     """A wrapper for get_common_string to return a dotted decimal IP
    #     address when print is called on an IPv4Address"""
    #     return self.get_common_string()
